################################################################################
##
## jscompressor.py
## (C) Numbler LLC 2006
##
## utility class for compressing javascript "on the fly"
##
## In debug mode script tags are returned for rendering by a nevow page.
##
## in release mode the state of the compressed JS image is returned.
##
## if the JS image is deemed to be out of date it is regenerated


import os,stat,subprocess,shutil,inspect
from nevow import tags,stan
import re

rhino_compressor = "dojo/buildscripts/lib/custom_rhino.jar"

def compressJs(sourceBuffer,prefix,destFile,filterExp = None):
    tmp = os.tmpnam()
    tfp = open(tmp,'w')
    tfp.write(sourceBuffer)
    tfp.close()

    dfp = open(destFile,'w')
    dfp.write(prefix)
    proc = subprocess.Popen(['java','-jar',rhino_compressor,'-c',tmp],stdout=subprocess.PIPE)
    shutil.copyfileobj(proc.stdout,dfp)
    dfp.flush()
    dfp.close()
    if filterExp:
        removed = re.sub(filterExp,'',open(destFile).read())
        dfp = open(destFile,'w')
        dfp.write(removed)
        dfp.close()
    os.unlink(tmp)
    

class JSCompressor(object):
    """
    JSCompressor is useful for managing a site with a large amount of
    javascript.  On production sites it is useful to 'compress' javascript so
    as to avoid excess connections, obsfucate code, avoid client timeout issues,
    and reduce bandwidth consumption.
    
    JSCompressor takes an array of tupes for each javascript file.  Each tuple represents an
    individual file or javascript block. the tuple is in the form (urlBase,fileBase,filename).
    if the tuple only has one value it assummed to be an inline script.
    
    example:

    jsdata = [
    # urlBase fileBase filename
    ('/dojo','./dojo','dojo.js')
    ('/js','./releasescripts/js','utils.js},
    ('window.onload = function() { alert('I was called on load')')
    ]

    release is a dict for release URL used in production.  The actual serving
    up of the URL is handled elsewhere.

    release = {'urlBase':'/releaseJS','fileBase':'./compressedJS','filename':'site.js'}

    the parent argument is used to specify the parent module which contains the jsdata. This is
    useful if you have inline JS and want to ensure that the generated file is refreshed
    if the source file is newer.
    
    """
    defPrefix = """/* Generated by JScompressor.  Do not edit! */\n\n"""
    jsType = 'text/javascript'
    def __init__(self,jsdata,release,parent=None,onTheFly=True,prefix = None,compressorFunc=None):

        self.jsdata = jsdata
        self.release = release
        self.statFiles = onTheFly
        self.prefix = prefix is not None and prefix or self.defPrefix
        self.compressFunc = compressorFunc is not None and compressorFunc or compressJs

        # called here to let us no if we are going to run into trouble later on.
        self._validate()

        # if not building dynamically generate the file now
        if not self.statFiles:
            self._generate()
        else:
            try:
                self.genTime = self.releaseModTime()
                if parent is not None:
                    pfile = inspect.getfile(parent)
                    pfiletime = os.stat(pfile)[stat.ST_MTIME]
                    if pfiletime > self.genTime:
                        print 'source file is newer, forcing regeneration'
                        self._generate()
            except OSError,e:
                # this is ok, means the file doesn't exist
                pass

            
        
    def _validate(self):
        lastMtime = 0
        for jsitem in self.jsdata:
            if len(jsitem) > 1:
                # stat will raise an error if the file(s) do not exist
                lastMtime =  max(lastMtime,
                                 os.stat(os.sep.join(jsitem[1:]))[stat.ST_MTIME])
        return lastMtime

    def releaseFile(self):
        return os.sep.join(self.release[1:])

    def releaseModTime(self):
        return os.stat(self.releaseFile())[stat.ST_MTIME]

    def _generate(self):
        buff = []
        for jsitem in self.jsdata:
            if len(jsitem) > 1:
                buff.append(open(os.sep.join(jsitem[1:])).read())
            else:
                buff.append(jsitem[0])

        # clear out \n and log statements
        #fullbuff = 
        #cleaner = re.compile(r'log(.*?);|\n')
        #compressbuff = re.sub(cleaner,'',fullbuff)
        #compressbuff = fullbuff
        
        self.compressFunc('\n'.join(buff),self.prefix,self.releaseFile(),re.compile(r'log\(.*?\);'))
        self.genTime = self.releaseModTime()

    def debugMode(self):
        ret = []
        for jsitem in self.jsdata:
            if len(jsitem) == 1:
                tag = tags.script(type=self.jsType)[tags.raw(jsitem[0])]
            else:
                tag = tags.script(type=self.jsType,src=os.sep.join([jsitem[0],jsitem[2]]))
            ret.append(tag)
        return ret

    def releaseMode(self):
        if self.statFiles:
            mtime = self._validate()
            try:
                gentime = self.releaseModTime()
            except OSError,e:
                gentime = 0
            if gentime < mtime:
                self._generate()

        return tags.script(type=self.jsType,src=os.sep.join([self.release[0],self.release[2]]))
 


def test():
    jsdata = [
        ('/js','/tmp/foo','sheet.js'),
        ('/js','/tmp/foo','cell.js')
        ]
    releaseData = ('/releaseJS','/tmp/foo/','output.js')

    compressor = JSCompressor(jsdata,releaseData,None,False)
    print compressor.debugMode()
    print compressor.releaseMode()
    

    onTheFlycompressor = JSCompressor(jsdata,releaseData,test)
    print onTheFlycompressor.debugMode()
    print onTheFlycompressor.releaseMode()    
    

    
def main():
    test()
    return
    
    import profile,pstats

    profile.run("test()","jscompress.dat")
    p = pstats.Stats("jscompress.dat")
    p.sort_stats('cumulative').print_stats(40)
    p.sort_stats('calls').print_stats(40)


    
if __name__ == "__main__":
	main()    
            
